package org.ihelpuoo.orm;

import java.lang.reflect.Field;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.ihelpuoo.db.sql.SQLUtils;
import org.ihelpuoo.lang.ObjectUtils;
import org.ihelpuoo.lang.StringUtils;
import org.ihelpuoo.orm.filter.FieldFilter;
import org.ihelpuoo.parser.segment.CharSegment;
import org.ihelpuoo.parser.segment.Segment;
import org.w3c.dom.Element;

/**
 * Store the field rule
 */
public class FieldRule
{
	class FieldRelation
	{
		Field	me;
		Field	that;
	}

	static final int		UNKNOWN			= 0;
	static final int		NORMAL			= 1;
	static final int		OBJECT			= 2;
	static final int		ARRAY			= 3;
	static final int		LIST			= 4;
	static final int		MAP				= 5;

	public static final int	ATTRIBUTE_NODE	= 1;
	public static final int	ELEMENT_NODE	= 2;
	public static final int	COMMENT_NODE	= 3;
	public static final int	TEXT_NODE		= 4;

	FieldRule(ObjectRule parent, Field f)
	{
		this.field = f;
		this.parent = parent;
		iamFirst = true;
	}

	private ObjectRule		parent;
	private Field			field;
	private String			nameInDB;
	private String			nameInDom;
	private int				type;
	private int				domNodeType;
	private Class			componentType;
	private String			alias;
	private Segment			filter;		// maybe we need XPath...

	private FieldRelation	relation;		// When type is not normal, maybe need this info.
	private boolean			iamFirst;		// if type is OBJECT, when insert, it will be considering.

	private boolean			fake;
	private boolean			autoIncrement;
	private boolean			writable;
	private boolean			searchable;
	private boolean			notNull;
	private boolean			PK;
	private boolean			newKey;

	private String			ignoreBy;

	public boolean isIamFirst()
	{
		return iamFirst;
	}

	/**
	 * Evaluate one field type by its config or 
	 * 
	 * @param r: the field rule
	 * @param fieldType: the field type
	 * @param conf
	 * @throws IIIException
	 */
	void evalNormalPart() throws IIIException
	{
		Element conf = null;
		alias = field.getName();
		if (null != parent.root)
		{
			try
			{
				conf = (Element)XPathFactory.newInstance().newXPath().evaluate(
						"./mapping/" + field.getName(),parent.root,XPathConstants.NODE);
			}
			catch (XPathExpressionException e)
			{
				throw new IIIException("./mapping/" + field.getName() + "\r\n::" + e.getMessage());
			}
			if (null != conf && conf.hasAttribute("alias"))
			{
				if (conf.hasAttribute("alias"))
					alias = conf.getAttribute("alias");
				if (conf.hasAttribute("ignore-by"))
					ignoreBy = conf.getAttribute("ignore-by");
			}

			/* Find the key field */
			if (null != conf && conf.hasAttribute("key")
					&& Boolean.parseBoolean(conf.getAttribute("key")))
				parent.keyField = field;
		}
		if (isNormalField(field.getType()))
		{
			type = NORMAL;
			if (null == ignoreBy && ObjectUtils.isInteger(getField().getType()))
				ignoreBy = String.valueOf(III.DEFAULT_OBJECT_ID);
		}
		else if (field.getType().isArray())
		{
			type = ARRAY;
			componentType = field.getType().getComponentType();
		}
		else if (ObjectUtils.isChildOf(field.getType(),List.class))
		{
			type = LIST;
			evalComponentType(conf);
		}
		else if (ObjectUtils.isChildOf(field.getType(),Map.class))
		{
			type = MAP;
			evalComponentType(conf);
		}
		else
		{
			type = OBJECT;
			componentType = field.getType();
		}
	}

	public String getIgnoreBy()
	{
		return ignoreBy;
	}

	void evalXmlPart(int len) throws XPathExpressionException
	{
		Element ele = null;
		if (null != parent.root)
		{
			ele = (Element)XPathFactory.newInstance().newXPath().evaluate(
					"./mapping/" + field.getName() + "/xml",parent.root,XPathConstants.NODE);
			if (null != ele)
			{
				nameInDom = ele.getAttribute("name");
				if (ele.hasAttribute("mode"))
				{
					if ("attribute".equalsIgnoreCase(ele.getAttribute("mode")))
						domNodeType = ATTRIBUTE_NODE;
					else if ("text".equalsIgnoreCase(ele.getAttribute("mode")))
						domNodeType = TEXT_NODE;
					else if ("comment".equalsIgnoreCase(ele.getAttribute("mode")))
						domNodeType = COMMENT_NODE;
					else
						domNodeType = ELEMENT_NODE;
				}
			}
		}
		if (StringUtils.isBlank(nameInDom))
		{
			if (ObjectUtils.isBoolean(field.getType()) && field.getName().startsWith("is"))
			{
				StringBuffer sb = new StringBuffer();
				sb.append(Character.toLowerCase(field.getName().charAt(2)));
				if (field.getName().length() > 3)
					sb.append(field.getName().substring(3));
				nameInDom = sb.toString();
			}
			else
				nameInDom = field.getName();
		}
		if (domNodeType == 0)
		{
			if (ObjectUtils.isPrimitive(field.getType()))
				domNodeType = ATTRIBUTE_NODE;
			else if ("name".equalsIgnoreCase(field.getName()))
				domNodeType = ATTRIBUTE_NODE;
			else if ("comment".equalsIgnoreCase(field.getName()))
				domNodeType = COMMENT_NODE;
			else if (len == 2 && "value".equalsIgnoreCase(field.getName()))
				domNodeType = TEXT_NODE;
			else
				domNodeType = ELEMENT_NODE;
		}
	}

	/**
	 * Evalue one field component type
	 * 
	 * @param r
	 * @throws IIIException
	 */
	private void evalComponentType(Element conf) throws IIIException
	{
		if (null != componentType)
			return;
		String suffix = null;
		String fullName = parent.getType().getSimpleName();
		if (fullName.length() != parent.getName().length())
			suffix = fullName.substring(parent.getName().length());
		try
		{
			if (null == conf || !conf.hasAttribute("type"))
				if (parent.getType().getPackage() != null)
					componentType = Class.forName(parent.getType().getPackage().getName() + '.'
							+ StringUtils.capitalize(StringUtils.plural2singular(field.getName()))
							+ (suffix == null ? "" : suffix));
				else
					componentType = Class.forName(StringUtils.capitalize(StringUtils
							.plural2singular(field.getName()))
							+ (suffix == null ? "" : suffix));
			else
				componentType = Class.forName(conf.getAttribute("type"));
		}
		catch (ClassNotFoundException e)
		{
			this.componentType = null;
			this.type = UNKNOWN;
		}
	}

	/**
	 * @param fieldType
	 * @return
	 * 	one field is normal or not, normal is mean
	 * 		int,long,float,double,byte,char,CharSequence,Calendar
	 */
	private static boolean isNormalField(Class fieldType)
	{
		if (ObjectUtils.isPrimitive(fieldType))
			return true;
		if (ObjectUtils.isChildOf(fieldType,CharSequence.class))
			return true;
		if (ObjectUtils.isChildOf(fieldType,Calendar.class))
			return true;
		if (ObjectUtils.isChildOf(fieldType,Timestamp.class))
			return true;
		if (ObjectUtils.isChildOf(fieldType,TimeZone.class))
			return true;
		return false;
	}

	void evalDbPart(Field f, Element root, ResultSet rs) throws SQLException,
			XPathExpressionException, IIIException
	{
		Element ele = null;
		if (null != root)
		{
			ele = (Element)XPathFactory.newInstance().newXPath().evaluate(
					"./mapping/" + f.getName() + "/db",root,XPathConstants.NODE);
			if (null != ele)
			{
				nameInDB = ele.getAttribute("name");
				/*
				 * Try to evaluate filter, if can not find filter for this field
				 * "fake" will be marked.
				 * OBJECT | ARRAY | LIST | MAP will be under considered.
				 */
				if (!isNormal())
					if (!StringUtils.isBlank(ele.getTextContent()))
						filter = new CharSegment(ele.getTextContent());
					else
						fake = true;
				// check PK
				if (ele.hasAttribute("pk"))
					PK = Boolean.parseBoolean(ele.getAttribute("pk"));
				// check New Key
				if (ele.hasAttribute("new"))
					newKey = Boolean.parseBoolean(ele.getAttribute("new"));
				// chekc relation
				if (ele.hasAttribute("relation"))
				{
					String s = ele.getAttribute("relation");
					int pos = s.indexOf(':');
					relation = new FieldRelation();
					relation.me = ObjectUtils.getField(this.parent.getType(),s.substring(0,pos));
					relation.that = ObjectUtils.getField(this.getComponentType(),s
							.substring(pos + 1));
				}
			}
		}
		if (nameInDB == null)
			nameInDB = StringUtils.lowercaseWordBy(f.getName(),parent.config.getWordSeparator());
		if (!isNormal())
		{
			fake = true;
			writable = false;
		}
		// check can fetch the object from resultset or not.
		// mark more info special for DB
		if (!fake)
		{
			ResultSetMetaData md = rs.getMetaData();
			int col = 0;
			for (int m = 1; m <= md.getColumnCount(); m++)
				if (md.getColumnName(m).equalsIgnoreCase(nameInDB))
				{
					col = m;
					break;
				}
			if (col == 0)
				fake = true;
			else
			{
				autoIncrement = md.isAutoIncrement(col);
				if (autoIncrement && !ObjectUtils.isInteger(f.getType()))
					throw new IIIException("'" + f.getType().getName() + "' field '" + f.getName()
							+ "' must be a byte|int|long [" + parent.getType().getName() + "]");
				writable = md.isWritable(col);
				searchable = md.isSearchable(col);
				notNull = (ResultSetMetaData.columnNoNulls == md.isNullable(col));
			}
		}
	}

	public boolean isWritable()
	{
		return writable;
	}

	void setWritable(boolean writable)
	{
		this.writable = writable;
	}

	public boolean isNewKey()
	{
		return newKey;
	}

	void setNewKey(boolean newKey)
	{
		this.newKey = newKey;
	}

	public Class getComponentType()
	{
		return componentType;
	}

	public String getNameInDB()
	{
		return nameInDB;
	}

	void setNameInDB(String nameInDB)
	{
		this.nameInDB = nameInDB;
	}

	public String getNameInDom()
	{
		return nameInDom;
	}

	public String getAlias()
	{
		return alias;
	}

	public String getName()
	{
		return field.getName();
	}

	public Field getField()
	{
		return field;
	}

	public boolean isNormal()
	{
		return type == NORMAL;
	}

	public boolean isUnknown()
	{
		return type == UNKNOWN;
	}

	public boolean isComponentType()
	{
		return !isNormal() && !isUnknown();
	}

	public boolean isArray()
	{
		return type == ARRAY;
	}

	public boolean isMap()
	{
		return type == MAP;
	}

	public boolean isList()
	{
		return type == LIST;
	}

	public boolean isObject()
	{
		return type == OBJECT;
	}

	public int getType()
	{
		return type;
	}

	public int getDomNodeType()
	{
		return domNodeType;
	}

	public Segment getFilter()
	{
		if (null == filter && null != relation)
		{
			try
			{
				String subNameInDb = parent.iii.getManual().getRule(componentType).getFieldRule(
						relation.that).getNameInDB();
				if (SQLUtils.isNotNeedQuote(relation.me.getType()))
					filter = new CharSegment(subNameInDb + "=${" + relation.me.getName() + "}");
				else
					filter = new CharSegment(subNameInDb + "='${" + relation.me.getName() + "}'");
			}
			catch (IIIException e)
			{
				throw new RuntimeException(e);
			}
		}
		return filter;
	}

	public boolean isFake()
	{
		return fake;
	}

	public boolean isAutoIncrement()
	{
		return autoIncrement;
	}

	public boolean isSearchable()
	{
		return searchable;
	}

	public boolean isNotNull()
	{
		return notNull;
	}

	public boolean isPK()
	{
		return PK;
	}

	ObjectRule getParent()
	{
		return parent;
	}

	void setParent(ObjectRule parent)
	{
		this.parent = parent;
	}

	void setField(Field field)
	{
		this.field = field;
	}

	void setNameInDom(String nameInDom)
	{
		this.nameInDom = nameInDom;
	}

	void setType(int type)
	{
		this.type = type;
	}

	void setDomNodeType(int nodeType)
	{
		this.domNodeType = nodeType;
	}

	void setComponentType(Class componentType)
	{
		this.componentType = componentType;
	}

	void setAlias(String alias)
	{
		this.alias = alias;
	}

	void setFake(boolean fake)
	{
		this.fake = fake;
	}

	void setAutoIncrement(boolean autoIncrement)
	{
		this.autoIncrement = autoIncrement;
	}

	void setSearchable(boolean searchable)
	{
		this.searchable = searchable;
	}

	void setNotNull(boolean notNull)
	{
		this.notNull = notNull;
	}

	void setPK(boolean pk)
	{
		PK = pk;
	}

	public boolean hasRelation()
	{
		return null != this.relation;
	}

	void setRelation(String me, String that)
	{
		if (this.isNormal())
			return;
		relation = new FieldRelation();
		relation.me = ObjectUtils.getField(parent.getType(),me);
		relation.that = ObjectUtils.getField(componentType,that);
		if (!parent.getFieldRule(relation.me).isAutoIncrement())
			this.iamFirst = false;
	}

	public Field getMe()
	{
		if (null == relation || isNormal())
			return null;
		return relation.me;
	}

	public Field getThat()
	{
		if (null == relation || isNormal())
			return null;
		return relation.that;
	}

	public FieldRule getThatRule()
	{
		if (null == relation || isNormal())
			return null;
		try
		{
			return parent.iii.manual.getRule(componentType).getFieldRule(relation.that);
		}
		catch (IIIException e)
		{
			throw new RuntimeException(e);
		}
	}

	public boolean isIgnore(FieldFilter actived, FieldFilter locked)
	{
		return FieldFilter.isIgnore(field,actived,locked);
	}

	public boolean isIgnoreInResultSet(FieldFilter actived, FieldFilter locked)
	{
		if (isUnknown())
			return true;
		if (isFake() && isNormal())
			return true;
		return isIgnore(actived,locked);
	}
}
